package asteroids.model;

import java.util.HashSet;

import be.kuleuven.cs.som.annotate.*;

import asteroids.CollisionListener;
import asteroids.Util;
import asteroids.Vector;
import asteroids.model.programs.Program;

/**
 * @version	1.0
 * @author 	Ewoud Van Esch & Jomi Verwimp
 */
//TODO class info
public class World {
	
	/**
	 * @param x
	 * @param y
	 * 
	 * @effect	| dimension = new Vector(x,y)
	 */
	public World(double x, double y) throws IllegalArgumentException {
		System.out.println(x);
		System.out.println(y);
		if( (x < 0) || (x > getUpperBound()) )
			throw new IllegalArgumentException("Worlds width cannot be less than zero or greater than "+getUpperBound()+"!");
		if( (y < 0) || (y > getUpperBound()) )
			throw new IllegalArgumentException("Worlds heigth cannot be less than zero or greater than "+getUpperBound()+"!");
		dimension = new Vector(x,y);
	}
	
	/**
	 * Get the dimension of this world as a vector,
	 * where x represents the width and y the height.
	 */
	@Basic
	public Vector getDimension() {
		return dimension;
	}

	/**
	 * Variable holding this worlds dimension.
	 */
	private final Vector dimension;
	
	/**
	 * @return 	| result == upperBound
	 */
	@Basic @Immutable
	public static double getUpperBound() {
		return upperBound;
	}
	
	private final static double upperBound = Double.MAX_VALUE;
	
	/**
	 * @return	| result == ships
	 */
	@Basic
	public HashSet<Ship> getAllShips() {
		return this.ships;
	}
	
	/**
	 * @param ship
	 * @trows IllegalArgumentException
	 * 
	 * @effect	
	 * 		| if (ship != null && !getAllShips().contains(ship))
	 * 		|	then (ship.setWorld(this) && ships.add(ship))
	 */
	public void add(Ship ship) throws IllegalArgumentException {
		if (ship == null) {
			throw new IllegalArgumentException("The ship to add cannot be null!");
		}
		if (!getAllShips().add(ship)) {
			throw new IllegalArgumentException("The ship is already part of the set!");
		}
		ship.setWorld(this);
	}
	
	/**
	 * @param ship
	 * @trows IllegalArgumentException
	 * 
	 * @effect	
	 * 		| if (ship != null && getAllShips().contains(ship))
	 * 		| 	then (ships.remove(ship) && ship.setWorld(null))
	 */
	public void remove(Ship ship) throws IllegalArgumentException {
		if (ship == null) {
			throw new IllegalArgumentException("The ship to remove cannot be null!");
		}
		if(!getAllShips().remove(ship)) {
			throw new IllegalArgumentException("There is no such ship in the set!");
		}
		ship.setWorld(null);
	}
	
	private HashSet<Ship> ships = new HashSet<Ship>();

	/**
	 * @return	| result == this.asteroids
	 */
	@Basic
	public HashSet<Asteroid> getAllAsteroids() {
		return this.asteroids;
	}
	
	/**
	 * @param asteroid
	 * @throws IllegalArgumentException
	 * 
	 * @effect
	 * 		| if (asteroid != null)
	 * 		| 	then (asteroid.setWorld(this) && asteroids.add(asteroid))
	 */
	public void add(Asteroid asteroid) throws IllegalArgumentException {
		if (asteroid == null) {
			throw new IllegalArgumentException("The asteroid to add cannot be null!");
		}
		if (!getAllAsteroids().add(asteroid)) {
			throw new IllegalArgumentException("The asteroid to add is already part of the set!");
		}
		asteroid.setWorld(this);
	}
	
	/**
	 * @param asteroid
	 * @trows IllegalArgumentException
	 * 
	 * @effect	
	 * 		| if (asteroid != null && getAllAsteroids().contains(asteroid))
	 * 		| 	then (asteroids.remove(asteroid) && asteroid.setWorld(null))
	 */
	public void remove(Asteroid asteroid) throws IllegalArgumentException {
		if (asteroid == null) {
			throw new IllegalArgumentException("The asteroid to remove cannot be null!");
		}
		if(!getAllAsteroids().remove(asteroid)) {
			throw new IllegalArgumentException("There is no such asteroid in the set!");
		}
		asteroid.setWorld(null);
	}
	
	private HashSet<Asteroid> asteroids = new HashSet<Asteroid>();
	
	/**
	 * @effect	| allLiveBullets = new HashSet<Bullet>()
	 * 			| for(Ship ship : getAllShips()) {
	 * 			|	allLiveBullets.addAll(ship.getLiveBullets())
	 * 			| }
	 */
	@Basic
	public HashSet<Bullet> getAllBullets() {
		HashSet<Bullet> allLiveBullets = new HashSet<Bullet>();
		for(Ship ship : getAllShips()) {
			allLiveBullets.addAll(ship.getLiveBullets());
		}
		return allLiveBullets;
	}
	
	/**
	 * @effect	| allPrograms = new HashSet<Program>()
	 * 			| for(Ship ship : getAllShips()) {
	 * 			|	if(ship.getProgram() != null)
	 * 			|		then allPrograms.add(ship.getProgram())
	 * 			| }
	 * @return	| result == allPrograms;
	 */
	public HashSet<Program> getAllPrograms() {
		HashSet<Program> allPrograms = new HashSet<Program>();
		for(Ship ship : getAllShips()) {
			if(ship.getProgram() != null)
				allPrograms.add(ship.getProgram());
		}
		return allPrograms;
	}
	
	/**
	 * @effect 	| new HashSet<GameObject> objects
	 * 			| objects.addAll(getAllShips())
	 * 			| objects.addAll(getAllAsteroids())
	 * 			| objects.addAll(getAllBullets())
	 * @return 	| result == objects
	 */
	public HashSet<GameObject> getAllGameObjects() {
		HashSet<GameObject> objects = new HashSet<GameObject>();
		objects.addAll(getAllShips());
		objects.addAll(getAllAsteroids());
		objects.addAll(getAllBullets());
		return objects;
	}
	
	/**
	 * @return | for each(GameObject obj1 : getAllGameObjects()){
	 *		   |	for each(GameObject obj2 : getAllGameObjects()){
	 *		   |		Collision col = obj1.getObjectCollision(obj2)
	 *		   |		double newtc = obj1.getTimeToCollision(obj2)
	 *		   |		if (Util.fuzzyLessThanOrEqualTo(newtc, tc)) {
	 *	  	   |			tc = newtc
	 *	       |			firstCol = col
	 *	       |		}
	 *	       |	}
	 *	       | }
	 *	       | result == firstcol
	 */
	public Collision getFirstObjectCollision() {
		Collision firstCol = new Collision(null, null, Double.POSITIVE_INFINITY, null);
		double tc = Double.POSITIVE_INFINITY;
		for (GameObject obj1 : getAllGameObjects()){
			for (GameObject obj2 : getAllGameObjects()){
				Collision col = obj1.getObjectCollision(obj2);
				double newtc = obj1.getTimeToCollision(obj2);
				if (Util.fuzzyLessThanOrEqualTo(newtc, tc)) {
					tc = newtc;
					firstCol = col;
				}
			}
		}
		return firstCol;
	}
	
	/**
	 * @return | for (GameObject obj : getAllGameObjects()) {
	 * 		   | 	Collision col = obj.getBoundaryCollision()
	 * 		   |	double newtbc = col.getTimeToCollision()
	 * 		   |	if (Util.fuzzyLessThanOrEqualTo(newtbc, tbc)) {
	 * 		   |		tbc = newtbc
	 * 		   |		firstCol = col
	 *         | 	} }
	 *         |	result == firstCol
	 */
	public Collision getFirstBoundaryCollision() {
		Collision firstCol = new Collision(null, null, Double.POSITIVE_INFINITY, null);
		double tbc = Double.POSITIVE_INFINITY;
		for (GameObject obj : getAllGameObjects()) {
			Collision col = obj.getBoundaryCollision();
			double newtbc = col.getTimeToCollision();
			if (Util.fuzzyLessThanOrEqualTo(newtbc, tbc)) {
				tbc = newtbc;
				firstCol = col;
			}
		}
		return firstCol;
	}
	
	/**
	 * @return | if(getFirstBoundaryCollision() =! null && getFirstObjectCollision() =! null)
	 * 		   | 	if(getFirstBoundaryCollision().getTimeToCollision() < getFirstObjectCollision())
	 * 		   |		then result == getFirstBoundaryCollision()
	 * 		   |	else result == getFirstObjectCollision()
	 */
	public Collision getFirstCollision() {
		Collision firstBC = getFirstBoundaryCollision();
		Collision firstOC = getFirstObjectCollision();
		if (firstBC == null) {
			if (firstOC == null) {
				return null;
			}
			else return firstOC;
		} else if (firstOC == null) {
			return firstBC;
		}
		if ( Util.fuzzyLessThanOrEqualTo(firstBC.getTimeToCollision(), firstOC.getTimeToCollision()) ) {
			return firstBC;
		}
		else {
			return firstOC;
		}
	}
	
	/**
	 * @param 	dt
	 * @param 	collisionListener
	 * @effect	| tc = getFirstCollision().getTimeToCollision()
	 * 			| if(Util.fuzzyLessThanOrEqualTo(dt,tc))
	 * 			|	then advance(dt)
	 * 			| else while(util.fuzzyLessThanOrEqualTo(tc,dt)) {
	 * 			| 		advance(tc)
	 * 			|		getFirstCollision().resolve(collisionListener)
	 * 			|		dt = dt - tc
	 * 			|		tc = getFirstCollision().getTimeToCollision()
	 * 			|		}
	 * 			| }
	 * 			| executeProgram(dt)
	 */
	public void evolve(double dt, CollisionListener collisionListener) {
		double tc = getFirstCollision().getTimeToCollision();
		if (Util.fuzzyLessThanOrEqualTo(dt, tc)) {
			advance(dt);
		}
		else {
			while(Util.fuzzyLessThanOrEqualTo(tc, dt)) {
				advance(tc);
				getFirstCollision().resolve(collisionListener);
				dt = dt - tc;
				tc = getFirstCollision().getTimeToCollision();
			}
		}
		executeProgram(dt);
	}
	
	/**
	 * @param 	dt
	 */
	//TODO meep?
	public void executeProgram(double dt) {
		remt = remt + dt;
		int n = (int)(remt/0.2);//laat decimale plaatsen 'vallen', rond af naar 0
		if(n != 0) {
			for(int i=0 ; i<n ; i++) {
				for(Program program : getAllPrograms()) {		
					program.resume();
				}
			}
			remt = remt - n*0.2;
		}
	}
	
	private double remt;
	
	/**
	 * @param 	t
	 * @effect	for(GameObject obj : getAllGameObjects()){
	 * 				obj.move(t)
	 * 				if(obj instanceof Ship)
	 * 					then obj.thrust(t)
	 * 			}
	 */
	public void advance(double t) {
		for (GameObject obj : getAllGameObjects()) {
			obj.move(t);
			if (obj instanceof Ship) { //TODO lelleke instanceof
				((Ship)obj).thrust(t);
			}
		}
	}
	
}

